# 使用 torch.autograd 进行自动微分

import torch

# 在训练神经网络时，最常用的算法是反向传播。在该算法中，参数（模型权重）根据损失函数相对于给定参数的梯度进行调整。

# 为了计算这些梯度，PyTorch 有一个内置的微分引擎，称为 torch.autograd。它支持自动计算任何计算图的梯度。

# 考虑最简单的一层神经网络，其输入为 x，参数为 w 和 b，以及一些损失函数。它可以在 PyTorch 中按以下方式定义



device = (
    "cuda"
    if torch.cuda.is_available()
    else "mps"
    if torch.backends.mps.is_available()
    else "cpu"
)
print(f"Using {device} device")



x = torch.ones(5,device=device)  # 输入张量
y = torch.zeros(3,device=device)  # 输出张量
# torch.randn是PyTorch库中的一个函数，它用于生成服从正态分布的随机数
# device参数指定了张量在CPU还是GPU上存储和计算
# requires_grad参数设置为True表示张量在计算图中需要跟踪梯度。
w = torch.randn(5,3,device=device,requires_grad=True)  # 权重张量（随机的）
b = torch.randn(3,device=device,requires_grad=True)    # 偏置张量（随机的）
z = torch.matmul(x, w)+b                               # 线性变换
loss = torch.nn.functional.binary_cross_entropy_with_logits(z,y)

# 在此网络中，w 和 b 是我们需要优化的**参数**。
# 因此，我们需要能够计算损失函数相对于这些变量的梯度。
# 为此，我们设置了这些张量的 requires_grad 属性。
# 您可以在创建张量时设置 requires_grad 的值，也可以稍后使用 x.requires_grad_(True) 方法进行设置。



print(x)
print(w)
print(y)

# 我们应用于张量以构建计算图的函数实际上是 Function 类的对象。
# 此对象知道如何在*前向*方向上计算函数，也知道如何在*反向传播*步骤中计算其导数。
# 对反向传播函数的引用存储在张量的 grad_fn 属性中。
# 您可以在文档中找到有关 Function 的更多信息。

# 我们可以打印 grad_fn 属性以查看其值。方法是： print(x.grad_fn)

print(f"Gradient function of z {z.grad_fn}")
print(f"Gradient function of loss {loss.grad_fn}")

# 为了优化神经网络中参数的权重，我们需要计算损失函数相对于参数的导数，即我们需要在 x 和 y 的某些固定值下计算。
# 为了计算这些导数，我们调用 loss.backward()，然后从 w.grad 和 b.grad 中检索值。

loss.backward()
print(w.grad)
print(b.grad)

# 我们只能获取计算图中叶节点的 grad 属性，这些节点的 requires_grad 属性设置为 True。
# 对于图中的所有其他节点，梯度将不可用。

# 出于性能原因，我们只能在给定图上使用 backward 进行一次梯度计算。
# 如果我们需要在同一个图上进行多次 backward 调用，我们需要将 retain_graph=True 传递给 backward 调用。

# 禁用梯度跟踪

print(z.requires_grad)

with torch.no_grad():
    z = torch.matmul(x, w)+b
print(z.requires_grad)

# 实现相同结果的另一种方法是在张量上使用 detach() 方法。

z_det = z.detach()
print(z_det.requires_grad)

# 您可能希望禁用梯度跟踪的原因有很多：
# 将神经网络中的某些参数标记为**冻结参数**。这些参数在训练期间不会更新。
# 当您只进行前向传递时**加速计算**，因为对不跟踪梯度的张量进行计算会更有效率。

